/*
 *  Licensed to the Apache Software Foundation (ASF) under one or more
 *  contributor license agreements.  See the NOTICE file distributed with
 *  this work for additional information regarding copyright ownership.
 *  The ASF licenses this file to You under the Apache License, Version 2.0
 *  (the "License"); you may not use this file except in compliance with
 *  the License.  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 */

package org.apache.tomcat.util.scan;

import org.apache.juli.logging.Log;
import org.apache.juli.logging.LogFactory;
import org.apache.tomcat.JarScanner;
import org.apache.tomcat.JarScannerCallback;
import org.apache.tomcat.util.ExceptionUtils;
import org.apache.tomcat.util.buf.UriUtil;
import org.apache.tomcat.util.file.Matcher;
import org.apache.tomcat.util.res.StringManager;

import javax.servlet.ServletContext;
import java.io.File;
import java.io.IOException;
import java.net.JarURLConnection;
import java.net.URL;
import java.net.URLClassLoader;
import java.net.URLConnection;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;
import java.util.StringTokenizer;

/**
 * The default {@link JarScanner} implementation scans the WEB-INF/lib directory
 * followed by the provided classloader and then works up the classloader
 * hierarchy. This implementation is sufficient to meet the requirements of the
 * Servlet 3.0 specification as well as to provide a number of Tomcat specific
 * extensions. The extensions are:
 * <ul>
 * <li>Scanning the classloader hierarchy (enabled by default)</li>
 * <li>Testing all files to see if they are JARs (disabled by default)</li>
 * <li>Testing all directories to see if they are exploded JARs
 * (disabled by default)</li>
 * </ul>
 * All of the extensions may be controlled via configuration.
 */
public class StandardJarScanner implements JarScanner {

	private static final Log log = LogFactory.getLog(StandardJarScanner.class);

	private static final Set<String> defaultJarsToSkip = new HashSet<String>();

	/**
	 * The string resources for this package.
	 */
	private static final StringManager sm =
			StringManager.getManager(Constants.Package);

	static {
		String jarList = System.getProperty(Constants.SKIP_JARS_PROPERTY);
		if (jarList != null) {
			StringTokenizer tokenizer = new StringTokenizer(jarList, ",");
			while (tokenizer.hasMoreElements()) {
				String token = tokenizer.nextToken().trim();
				if (token.length() > 0) {
					defaultJarsToSkip.add(token);
				}
			}
		}
	}

	/**
	 * Controls the classpath scanning extension.
	 */
	private boolean scanClassPath = true;
	/**
	 * Controls the testing all files to see of they are JAR files extension.
	 */
	private boolean scanAllFiles = false;
	/**
	 * Controls the testing all directories to see of they are exploded JAR
	 * files extension.
	 */
	private boolean scanAllDirectories = false;
	/**
	 * Controls the testing of the bootstrap classpath which consists of the
	 * runtime classes provided by the JVM and any installed system extensions.
	 */
	private boolean scanBootstrapClassPath = false;

	public boolean isScanClassPath() {
		return scanClassPath;
	}

	public void setScanClassPath(boolean scanClassPath) {
		this.scanClassPath = scanClassPath;
	}

	public boolean isScanAllFiles() {
		return scanAllFiles;
	}

	public void setScanAllFiles(boolean scanAllFiles) {
		this.scanAllFiles = scanAllFiles;
	}

	public boolean isScanAllDirectories() {
		return scanAllDirectories;
	}

	public void setScanAllDirectories(boolean scanAllDirectories) {
		this.scanAllDirectories = scanAllDirectories;
	}

	public boolean isScanBootstrapClassPath() {
		return scanBootstrapClassPath;
	}

	public void setScanBootstrapClassPath(boolean scanBootstrapClassPath) {
		this.scanBootstrapClassPath = scanBootstrapClassPath;
	}

	/**
	 * Scan the provided ServletContext and classloader for JAR files. Each JAR
	 * file found will be passed to the callback handler to be processed.
	 *
	 * @param context     The ServletContext - used to locate and access
	 *                    WEB-INF/lib
	 * @param classloader The classloader - used to access JARs not in
	 *                    WEB-INF/lib
	 * @param callback    The handler to process any JARs found
	 * @param jarsToSkip  List of JARs to ignore. If this list is null, a
	 *                    default list will be read from the system property
	 *                    defined by {@link Constants#SKIP_JARS_PROPERTY}
	 */
	@Override
	public void scan(ServletContext context, ClassLoader classloader,
	                 JarScannerCallback callback, Set<String> jarsToSkip) {

		if (log.isTraceEnabled()) {
			log.trace(sm.getString("jarScan.webinflibStart"));
		}

		final Set<String> ignoredJars;
		if (jarsToSkip == null) {
			ignoredJars = defaultJarsToSkip;
		} else {
			ignoredJars = jarsToSkip;
		}

		// Scan WEB-INF/lib
		Set<String> dirList = context.getResourcePaths(Constants.WEB_INF_LIB);
		if (dirList != null) {
			Iterator<String> it = dirList.iterator();
			while (it.hasNext()) {
				String path = it.next();
				if (path.endsWith(Constants.JAR_EXT) &&
						!Matcher.matchName(ignoredJars,
								path.substring(path.lastIndexOf('/') + 1))) {
					// Need to scan this JAR
					if (log.isDebugEnabled()) {
						log.debug(sm.getString("jarScan.webinflibJarScan", path));
					}
					URL url = null;
					try {
						// File URLs are always faster to work with so use them
						// if available.
						String realPath = context.getRealPath(path);
						if (realPath == null) {
							url = context.getResource(path);
						} else {
							url = (new File(realPath)).toURI().toURL();
						}
						process(callback, url);
					} catch (IOException e) {
						log.warn(sm.getString("jarScan.webinflibFail", url), e);
					}
				} else {
					if (log.isTraceEnabled()) {
						log.trace(sm.getString("jarScan.webinflibJarNoScan", path));
					}
				}
			}
		}

		// Scan the classpath
		if (scanClassPath && classloader != null) {
			if (log.isTraceEnabled()) {
				log.trace(sm.getString("jarScan.classloaderStart"));
			}

			ClassLoader loader = classloader;

			ClassLoader stopLoader = null;
			if (!scanBootstrapClassPath) {
				// Stop when we reach the bootstrap class loader
				stopLoader = ClassLoader.getSystemClassLoader().getParent();
			}

			while (loader != null && loader != stopLoader) {
				if (loader instanceof URLClassLoader) {
					URL[] urls = ((URLClassLoader) loader).getURLs();
					for (int i = 0; i < urls.length; i++) {
						// Extract the jarName if there is one to be found
						String jarName = getJarName(urls[i]);

						// Skip JARs known not to be interesting and JARs
						// in WEB-INF/lib we have already scanned
						if (jarName != null &&
								!(Matcher.matchName(ignoredJars, jarName) ||
										urls[i].toString().contains(
												Constants.WEB_INF_LIB + jarName))) {
							if (log.isDebugEnabled()) {
								log.debug(sm.getString("jarScan.classloaderJarScan", urls[i]));
							}
							try {
								process(callback, urls[i]);
							} catch (IOException ioe) {
								log.warn(sm.getString(
										"jarScan.classloaderFail", urls[i]), ioe);
							}
						} else {
							if (log.isTraceEnabled()) {
								log.trace(sm.getString("jarScan.classloaderJarNoScan", urls[i]));
							}
						}
					}
				}
				loader = loader.getParent();
			}

		}
	}

	/*
	 * Scan a URL for JARs with the optional extensions to look at all files
	 * and all directories.
	 */
	private void process(JarScannerCallback callback, URL url)
			throws IOException {

		if (log.isTraceEnabled()) {
			log.trace(sm.getString("jarScan.jarUrlStart", url));
		}

		URLConnection conn = url.openConnection();
		if (conn instanceof JarURLConnection) {
			callback.scan((JarURLConnection) conn);
		} else {
			String urlStr = url.toString();
			if (urlStr.startsWith("file:") || urlStr.startsWith("jndi:") ||
					urlStr.startsWith("http:") || urlStr.startsWith("https:")) {
				if (urlStr.endsWith(Constants.JAR_EXT)) {
					URL jarURL = UriUtil.buildJarUrl(urlStr);
					callback.scan((JarURLConnection) jarURL.openConnection());
				} else {
					File f;
					try {
						f = new File(url.toURI());
						if (f.isFile() && scanAllFiles) {
							// Treat this file as a JAR
							URL jarURL = UriUtil.buildJarUrl(f);
							callback.scan((JarURLConnection) jarURL.openConnection());
						} else if (f.isDirectory() && scanAllDirectories) {
							File metainf = new File(f.getAbsoluteFile() +
									File.separator + "META-INF");
							if (metainf.isDirectory()) {
								callback.scan(f);
							}
						}
					} catch (Throwable t) {
						ExceptionUtils.handleThrowable(t);
						// Wrap the exception and re-throw
						IOException ioe = new IOException();
						ioe.initCause(t);
						throw ioe;
					}
				}
			}
		}

	}

	/*
	 * Extract the JAR name, if present, from a URL
	 */
	private String getJarName(URL url) {

		String name = null;

		String path = url.getPath();
		int end = path.indexOf(Constants.JAR_EXT);
		if (end != -1) {
			int start = path.lastIndexOf('/', end);
			name = path.substring(start + 1, end + 4);
		} else if (isScanAllDirectories()) {
			int start = path.lastIndexOf('/');
			name = path.substring(start + 1);
		}

		return name;
	}

}
